﻿using System;
using System.Xml;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Globalization;
using AMO = Microsoft.AnalysisServices;

namespace AmoExtensions
{
    /*
     * SSAS Partition Manager
     * https://ssaspartitionmanager.codeplex.com/
     * Written by Dr. John Tunnicliffe of Decision Analytics, April 2015
     * eMail: john@decision-analytics.co.uk 
     * http://www.decision-analytics.co.uk/
     * https://www.linkedin.com/in/drjohntunnicliffe
     */

    /// <summary>
    /// Provides a wrapper around the AMO Server object including some useful methods
    /// </summary>
    public class AmoServer : IAmoServer, IDisposable
    {
        #region Constructor
        /// <summary>
        /// The OLAP server
        /// </summary>
        private readonly AMO.Server _server;

        /// <summary>
        /// Initializes a new instance of the AmoServer class.
        /// </summary>
        public AmoServer()
        {
            this._server = new AMO.Server();
        }
        #endregion

        public AMO.Server Server { get {  return _server; } }

        #region Name Property
        /// <summary>
        /// Gets the name of the server.  Note that this may not be the same as the server name we past to the Connect method as we can pass in an 
        /// DNS alias and AMO will return the real server name.
        /// </summary>
        /// <value>The name of the server.</value>
        public string Name
        {
            get { return this._server.Name.ToLowerInvariant(); }
        }
        #endregion

        #region ServerMode
        /// <summary>
        /// Returns the Server mode which is either Multidimensional or Tabular
        /// </summary>
        public string ServerMode
        {
            get 
            {
                switch (this._server.ServerMode)
                {
                    case AMO.ServerMode.Multidimensional:
                        return "Multidimensional";
                    case AMO.ServerMode.Tabular:
                    case AMO.ServerMode.SharePoint:
                        return "Tabular";
                }
                return "Multidimensional";
            }
        }
        #endregion

        #region Connect
        /// <summary>
        /// Connects the specified server name.
        /// </summary>
        /// <param name="serverName">Name of the server.</param>
        /// <returns><c>true</c> if we connected to the server correctly, <c>false</c> otherwise.</returns>
        public Boolean Connect(string serverName)
        {
            if (serverName.IsNullOrEmptyOrWhitespace()) throw new ArgumentNullException("serverName");

            // this will raise a ConnectionException if the server does not exist or does not run SSAS
            this._server.Connect(serverName);

            return Connected;
        }
        #endregion

        #region Connected
        /// <summary>
        /// Returns true if the class is connected to an OLAP server
        /// </summary>
        public bool Connected
        {
            get
            {
                return this._server.Connected;
            }
        }
        #endregion

        #region Database
        /// <summary>
        /// Finds a database on the server with a specific name or ID
        /// </summary>
        /// <remarks>Returns null if the database can't be found.</remarks>
        /// <param name="databaseNameOrId">The name or ID of the database to find.</param>
        /// <returns>A <c>Database</c> instance.</returns>
        public AMO.Database Database(string databaseNameOrId)
        {
            return(_server.Database(databaseNameOrId));
        }
        #endregion

        #region Cube
        /// <summary>
        /// Finds a cube on the server with a specific name or ID
        /// </summary>
        /// <remarks>Returns null if the cube can't be found.</remarks>
        /// <param name="databaseNameOrId">The name or ID of the database to find.</param>
        /// <param name="cubeNameOrId">The name or ID of the cube to find.</param>
        /// <returns>A <c>Cube</c> instance.</returns>
        public AMO.Cube Cube(string databaseNameOrId, string cubeNameOrId)
        {
            AMO.Cube amoCube = null;
            AMO.Database amoDatabase = Database(databaseNameOrId);
            if (amoDatabase != null)
            {
                amoCube = amoDatabase.Cube(cubeNameOrId);
            }
            return (amoCube);
        }
        #endregion

        #region MeasureGroup
        /// <summary>
        /// Finds a MeasureGroup on the server with a specific name or ID
        /// </summary>
        /// <remarks>Returns null if the MeasureGroup can't be found.</remarks>
        /// <param name="databaseNameOrId">The name or ID of the database to find.</param>
        /// <param name="cubeNameOrId">The name or ID of the cube to find.</param>
        /// <param name="measureGroupNameOrId">The name or ID of the MeasureGroup to find.</param>
        /// <returns>A <c>MeasureGroup</c> instance.</returns>
        public AMO.MeasureGroup MeasureGroup(string databaseNameOrId, string cubeNameOrId, string measureGroupNameOrId)
        {
            AMO.MeasureGroup amoMeasureGroup = null;
            AMO.Cube amoCube = Cube(databaseNameOrId, cubeNameOrId);
            if (amoCube != null)
            {
                amoMeasureGroup = amoCube.MeasureGroup(measureGroupNameOrId);
            }
            return (amoMeasureGroup);
        }
        #endregion

        #region StartCaptureLog
        /// <summary>
        /// Extension method to start capturing the XMLA commands produced by other AMO methods
        /// </summary>
        public void StartCaptureLog()
        {
            this._server.CaptureLog.Clear();
            this._server.CaptureXml = true;
        }
        #endregion

        #region ClearCaptureLog
        /// <summary>
        /// Extension method to clear capturing the XMLA commands produced by other AMO methods
        /// </summary>
        public void ClearCaptureLog()
        {
            this._server.CaptureLog.Clear();
            this._server.CaptureXml = false;
        }
        #endregion

        #region ExecuteCaptureLog
        /// <summary>
        /// Executes the captured XMLA processing commands
        /// </summary>
        /// <param name="transactional">True: Process within a transaction</param>
        /// <param name="parallel">True: Process in parallel</param>
        public void ExecuteCaptureLog(Boolean transactional, Boolean parallel)
        {
            AMO.XmlaResultCollection resultCollection = this._server.ExecuteCaptureLog(transactional, parallel);

            this.ClearCaptureLog();

            this.ProcessResultCollection(resultCollection);
        }

        /// <summary>
        /// Executes the captured XMLA processing commands
        /// </summary>
        /// <param name="transactional">Process within a transaction</param>
        /// <param name="parallel">True: Process in parallel</param>
        /// <param name="processAffected">Process any other objects affected by the processing of the main object</param>
        public void ExecuteCaptureLog(Boolean transactional, Boolean parallel, Boolean processAffected)
        {
            AMO.XmlaResultCollection resultCollection = this._server.ExecuteCaptureLog(transactional, parallel, processAffected);

            this.ClearCaptureLog();

            this.ProcessResultCollection(resultCollection);
        }
        #endregion

        #region ProcessResultCollection
        /// <summary>
        /// Processes the errors in the XMLA Result Collection
        /// </summary>
        /// <param name="resultCollection">The return from the Execute Capture Log</param>
        internal void ProcessResultCollection(AMO.XmlaResultCollection resultCollection)
        {
            // Iterate through the results collection and raises an exception if any XMLA Error messages found
            var exceptions = resultCollection
                .Cast<AMO.XmlaResult>()
                .SelectMany(x => x.Messages.Cast<AMO.XmlaMessage>())
                .Select(this.CheckXmlaMessageForProcessingError);

            var aggregateException = new AggregateException(exceptions);

            if (aggregateException.InnerExceptions.Count > 0)
            {
                throw aggregateException;
            }
        }
        #endregion 

        #region CheckXmlaMessageForProcessingError is a helper function for ProcessResultCollection
        /// <summary>
        /// Checks what type of XMLAMessage we have and writes log entries as appropriate
        /// </summary>
        /// <param name="xmlaMessage">The XMLAMessage</param>
        /// <returns>Exception object for each XMLAError encountered</returns>
        internal Exception CheckXmlaMessageForProcessingError(AMO.XmlaMessage xmlaMessage)
        {
            //string msg = string.Format(CultureInfo.InvariantCulture, "{0} {1}: {2}", DateTime.UtcNow.ToString("yyyy-MMM-dd hh:mm:ss"), xmlaMessage.GetType().Name, xmlaMessage.Description);
            string msg = string.Format("{0}: {1}", xmlaMessage.GetType().Name, xmlaMessage.Description);
            return new AMO.AmoException(msg);
        }
        #endregion

        #region DropDatabase
        /// <summary>
        /// Deletes the OLAP database held on the server
        /// If the database does not exist, then it does not raise an error.
        /// </summary>
        /// <param name="SsasDatabaseName"></param>
        public bool DropDatabase(string SsasDatabaseName)
        {
            bool returnValue = false;
            try
            {
                AMO.Database amoDb = this.Database(SsasDatabaseName);
                amoDb.Drop();
                returnValue = true;
            }
            catch (ApplicationException)
            {
                // the database did not exist in the first place, so we have been successfull!
                returnValue = true;
            }
            return returnValue;
        }
        #endregion

        #region UpdateCube
        /// <summary>
        /// Updates the cube so all the partitions are committed to the cube structure
        /// </summary>
        /// <param name="databaseNameOrId">Name or ID of the OLAP database</param>
        /// <param name="cubeNameOrId">Name or ID of the OLAP cube</param>
        public void UpdateCube(string databaseNameOrId, string cubeNameOrId)
        {
            AMO.Cube cube = this.Cube(databaseNameOrId, cubeNameOrId);
            cube.Update(AMO.UpdateOptions.ExpandFull);
        }
        #endregion

        #region ProcessDatabase
        public void ProcessDatabase(string SsasDatabaseName)
        {
            AMO.Database db = Database(SsasDatabaseName);
            StartCaptureLog();
            db.Process(AMO.ProcessType.ProcessFull);
            ExecuteCaptureLog(true, true);            
        }
        #endregion

        #region DeployDatabase
        /// <summary>
        /// Deploys a cube definition from an asDatabase file.
        /// This avoids using the Analysis Services Deployment Wizard or AmoCmd to do something similar.
        /// The code is taken directly from my old blog post:
        /// http://sqlblogcasts.com/blogs/drjohn/archive/2011/02/07/ssis-deploying-olap-cubes-using-c-script-tasks-and-amo.aspx
        /// </summary>
        /// <param name="SsasDatabaseName">Name of the new OLAP database. Note that if it does exists, it will be deleted!</param>
        /// <param name="fullPathToAsDatabaseDefinition">Full path to the AsDatabase file</param>
        /// <param name="connectionStringForCubeToSourceDataMart">The connection string to the </param>
        /// <returns>True if the database was deployed succesfully</returns>
        public bool DeployDatabase(
            string SsasDatabaseName,
            string fullPathToAsDatabaseDefinition,
            string connectionStringForCubeToSourceDataMart)
        {
            if (SsasDatabaseName.IsNullOrEmptyOrWhitespace()) throw new ArgumentNullException("SsasDatabaseName");
            if (fullPathToAsDatabaseDefinition.IsNullOrEmptyOrWhitespace()) throw new ArgumentNullException("fullPathToAsDatabaseDefinition");

            if (File.Exists(fullPathToAsDatabaseDefinition))
            {
                DropDatabase(SsasDatabaseName);

                string batchCmd = string.Format("<Batch Transaction='false' xmlns='http://schemas.microsoft.com/analysisservices/2003/engine'><Alter AllowCreate='true' ObjectExpansion='ExpandFull'><Object><DatabaseID>{0}</DatabaseID></Object><ObjectDefinition/></Alter></Batch>", SsasDatabaseName);

                // Start generating the main part of the XMLA command
                XmlDocument xmlaBatchCommand = new XmlDocument();
                xmlaBatchCommand.LoadXml(batchCmd);

                // To work with the XML and XPath we need an XmlNamespaceManager 
                NameTable nt = new NameTable();
                XmlNamespaceManager nsManager = new XmlNamespaceManager(xmlaBatchCommand.NameTable);
                nsManager.AddNamespace("ns1", "http://schemas.microsoft.com/analysisservices/2003/engine");
                nsManager.AddNamespace("ns2", "http://schemas.microsoft.com/analysisservices/2010/engine/200");

                // Find the ObjectDefinition node and add the cube definition file as a child of this node
                XmlNode oaRootNode = xmlaBatchCommand.SelectSingleNode("//ns1:ObjectDefinition", nsManager);
                if (oaRootNode != null)
                {
                    // load OLAP Database definition from the .asdatabase file identified by the AsDatabaseDefinitionFile file connection
                    XmlDocument olapCubeDef = new XmlDocument();
                    olapCubeDef.Load(fullPathToAsDatabaseDefinition);
                    // merge the two XML files by obtain a reference to the ObjectDefinition node 
                    oaRootNode.InnerXml = olapCubeDef.InnerXml;
                }

                // now remove all the nodes that cause errors when we deploy
                RemoveAllReadOnlyNodes(xmlaBatchCommand.DocumentElement, nsManager);

                // change the name of the OLAP database in the two nodes (ID and Name)
                XmlNode databaseID = xmlaBatchCommand.SelectSingleNode("//ns1:Database/ns1:ID", nsManager);
                if (databaseID != null)
                    databaseID.InnerText = SsasDatabaseName;
                XmlNode databaseName = xmlaBatchCommand.SelectSingleNode("//ns1:Database/ns1:Name", nsManager);
                if (databaseName != null)
                    databaseName.InnerText = SsasDatabaseName;

                // change the connection string to the relational database if we were provided with a new connection string
                if (!connectionStringForCubeToSourceDataMart.IsNullOrEmptyOrWhitespace())
                {
                    XmlNode connectionStringNode = xmlaBatchCommand.SelectSingleNode("//ns1:DataSources/ns1:DataSource/ns1:ConnectionString", nsManager);
                    if (connectionStringNode != null)
                    {
                        connectionStringNode.InnerText = connectionStringForCubeToSourceDataMart;
                    }
                }

                // change the impersonation mode of connection string to use the service account
                XmlNode impersonationNode = xmlaBatchCommand.SelectSingleNode("//ns1:DataSources/ns1:DataSource/ns1:ImpersonationInfo/ns1:ImpersonationMode", nsManager);
                if (impersonationNode != null)
                {
                    impersonationNode.InnerText = "ImpersonateServiceAccount";
                }

                // now deploy the new cube (structure only, no processing)
                AMO.XmlaResultCollection oResults = _server.Execute(xmlaBatchCommand.InnerXml);
                
                // check for errors during deployment
                // this will raise an AggregateException if anything went wrong
                ProcessResultCollection(oResults);
                
                // refresh the server objects view of the OLAP server
                _server.Refresh();

                return true;
            }
            else
            {
                throw new ApplicationException("Failed to find asdatabase file: " + fullPathToAsDatabaseDefinition);
            }
        }

        /// <summary>
        /// Deploys a cube definition from an asDatabase file.
        /// </summary>
        /// <param name="SsasDatabaseName">Name of the new OLAP database. Note that if it does exists, it will be deleted!</param>
        /// <param name="fullPathToAsDatabaseDefinition">Full path to the AsDatabase file</param>
        /// <returns>True if the database was deployed succesfully</returns>
        public bool DeployDatabase(
            string SsasDatabaseName,
            string fullPathToAsDatabaseDefinition)
        {
            return DeployDatabase(SsasDatabaseName, fullPathToAsDatabaseDefinition, null);
        }
        #endregion

        #region RemoveAllReadOnlyNodes is a helper function for DeployCube
        /// <summary>
        /// Visual studio adds a lot of XML nodes which Analysis Services Deployment Wizard removes automatically.
        /// If these nodes are not removed, the deployment will fail.
        /// See my blog entry for more information
        /// http://sqlblogcasts.com/blogs/drjohn/archive/2011/02/07/ssis-deploying-olap-cubes-using-c-script-tasks-and-amo.aspx
        /// </summary>
        /// <param name="rootNode"></param>
        /// <param name="nsManager"></param>
        private void RemoveAllReadOnlyNodes(XmlNode rootNode, XmlNamespaceManager nsManager)
        {
            #region Remove all the nodes that cause errors when we deploy
            foreach (XmlNode node in rootNode.SelectNodes("//ns1:CreatedTimestamp", nsManager))
            {
                node.ParentNode.RemoveChild(node);
            }
            foreach (XmlNode node in rootNode.SelectNodes("//ns1:LastSchemaUpdate", nsManager))
            {
                node.ParentNode.RemoveChild(node);
            }
            foreach (XmlNode node in rootNode.SelectNodes("//ns1:LastUpdate", nsManager))
            {
                node.ParentNode.RemoveChild(node);
            }
            foreach (XmlNode node in rootNode.SelectNodes("//ns1:LastProcessed", nsManager))
            {
                node.ParentNode.RemoveChild(node);
            }
            foreach (XmlNode node in rootNode.SelectNodes("//ns1:State", nsManager))
            {
                node.ParentNode.RemoveChild(node);
            }
            foreach (XmlNode node in rootNode.SelectNodes("//ns1:ImpersonationInfoSecurity", nsManager))
            {
                node.ParentNode.RemoveChild(node);
            }
            foreach (XmlNode node in rootNode.SelectNodes("//ns1:CurrentStorageMode", nsManager))
            {
                node.ParentNode.RemoveChild(node);
            }
            foreach (XmlNode node in rootNode.SelectNodes("//ns2:ProcessingState", nsManager))
            {
                node.ParentNode.RemoveChild(node);
            }
            foreach (XmlNode node in rootNode.SelectNodes("//ns1:ConnectionStringSecurity", nsManager))
            {
                node.ParentNode.RemoveChild(node);
            }
            #endregion
        }
        #endregion

        #region Dispose
        /// <summary>
        /// Close the connection to the server by calling Dispose()
        /// </summary>
        public void Dispose()
        {
            this._server.Dispose();
        }
        #endregion
    }
}
